Skip to content

fixing/merge-conflict - #1

Open
Pavitra-programmers wants to merge 4116 commits into
Pavitra-programmers:merge-conflictfrom
different-ai:dev
Open

fixing/merge-conflict#1
Pavitra-programmers wants to merge 4116 commits into
Pavitra-programmers:merge-conflictfrom
different-ai:dev

Conversation

@Pavitra-programmers

Copy link
Copy Markdown
Owner

Summary

Why

Issue

  • Closes #

Scope

Out of scope

Testing

Ran

  • ...

Result

  • pass/fail:
  • if fail, exact files/errors:

CI status

  • pass:
  • code-related failures:
  • external/env/auth blockers:

Manual verification

Evidence

  • video/screenshot link, or N/A (docs-only)

Risk

Rollback

benjaminshafii and others added 30 commits July 25, 2026 14:04
Rebuilds step 2 of the organization install guide around the fresh-install
case, matching the approved Paper design:

- Eyebrow, "Open the file you just downloaded" heading, a file chip naming
  the real installer artifact, two numbered actions, a calm amber OS trust
  note, a handoff note, an OR divider, the demoted already-installed row
  with the single dark primary button, and a quiet manual-paste disclosure.
- Copy is per-OS (macOS / Windows / Linux) and follows the platform the user
  actually downloaded, falling back to browser platform detection.
- The file chip shows the real published artifact name from
  genericInstallerArtifactName() (no version, because the published assets
  carry none) and is omitted when the platform or arch is unknown, so the
  page never invents a filename.
- The handoff note describes what the installer really does instead of
  promising that this page auto-advances to step 3, which it cannot observe:
  the installer resolves its own activation grant from the install token, so
  the web page's code is a different code.
- Installer preview: fake CTA flattened to a hairline outline, badge
  relabelled "PREVIEW · WHAT YOU'LL SEE ON YOUR COMPUTER" with a neutral
  dot, and the column stretches to match the left column height.

Eval flows are updated for the new copy and for the manual-paste disclosure.

Co-authored-by: Cursor <cursoragent@cursor.com>
…#3107)

The expired-install-link assertion pinned copy that no longer shipped, so
the suite failed on a clean checkout. Assert the actual contract (no raw
API error code leaks, the message names the link, expiry, and who to ask)
so wording can improve without breaking the test.
The installer artifact shipped in v0.18.1 reported 1.0.0 because the mac
Info.plist hardcoded it and nothing else carried a version at all, so a
user or support engineer could not tell which build they had.

- resolve one version from the release tag (local builds fall back to
  apps/installer/package.json) and stamp it into the compiled binary,
  the mac Info.plist, the DMG volume name, and the Windows version
  resource
- add `--version` to the installer CLI and a quiet footer in the window
- keep apps/installer/package.json on the release train via bump-version

Published asset filenames are unchanged.
Step 2 told macOS users to drag the app into Applications. The disk image
contains a single item, Install OpenWork, and no Applications shortcut, so
there is nothing to drag: you double-click that installer and choose Install.

Also stop attributing the installer's work to the app. The downloaded file is
the installer, it is what the OS warns about, and it is what installs the app
and opens the browser to approve this computer.

Co-authored-by: Cursor <cursoragent@cursor.com>
…#3120)

The flow still asserted the pre-DMG world: zip assets named
openwork-installer-mac-arm64.zip, an "OpenWork Installer.app" inside them, a
publish-generic-installer job, and a HEAD probe in the artifact resolver. None
of those exist now, so four assertions could only fail and the download path
could not validate a real artifact at all.

Validate the published disk images instead: check the UDIF trailer everywhere,
and on macOS verify, mount, and assert "Install OpenWork.app" at the image root
with codesign, Gatekeeper and notarization. Replace the closed v0.17.19
recovery step with the releases/latest URL den-api actually redirects to, and
point the pipeline assertions at the reusable workflow that owns the asset
names plus the download route that resolves them.

The version stamp is only asserted for the prerelease built from the pull
request, because releases up to and including v0.18.1 predate that fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t path (#3122)

An external MCP capability got 30s for the whole session -- connect, OAuth
refresh, initialize and tools/call shared one budget -- and execute_capability
spent that budget twice, once for schema-digest discovery and once for the
call. Slow providers died at ~31s.

The 120s/150s tool budget from #2750 was never reached. #2810 deleted the
legacy runtime, so callExternalMcpTool resolves to the enterprise client, and
the adapter's tool-call path forwarded no lifecycle deadline -- unlike
listExternalMcpTools beside it -- leaving the package's 30s default in charge.
A Math.min(operationTimeoutMs, ...) clamp meant injecting the longer deadline
would have been clipped back to 30s anyway. The only test asserting 120s
exercised the bypassed file.

Make an injected lifecycle deadline authoritative in both directions and
demote operationTimeoutMs to the fallback default. Forward the deadline on the
tool-call path, and share one deadline between discovery and the call so the
handshake and the budget are paid once. Split the per-request timeout from the
absolute bound and stop cancelling a provider that is still reporting
progress, matching the calling harness instead of being stricter than it; the
SDK only attaches a progressToken when an onprogress handler is present, so
the flag alone was inert.
Every dev Electron instance shared one profile directory, so
app.requestSingleInstanceLock() failed for the second worktree. Chromium had
already opened the CDP port by then, so the process lingered answering
/json/version without ever creating a window or logging anything - it looked
exactly like a hang.

Add OPENWORK_DEV_PROFILE (dev-only, inert when packaged): unset keeps today's
shared profile so existing profiles are untouched, 'auto' derives a stable name
from the worktree path, and any other value names a profile explicitly.
OPENWORK_ELECTRON_USERDATA still wins.

On lock contention the second dev instance now explains which profile directory
is held and how to isolate, then exits for real so the CDP port is released.
Dev startup prints the resolved profile directory and CDP address.

Add pnpm dev:worktree for the common case: auto profile plus free CDP and Vite
ports, so several worktrees run side by side.
…fetch (#3127)

* fix(desktop): route updater manifest through OS trust and guard bare fetch

The architecture-mismatch banner fetched the electron-updater manifest from
github.com with bare Node fetch, so it only trusted bundled Mozilla roots and
ignored system proxy configuration. On a TLS-inspecting corporate network that
single call failed while the rest of the app worked, with no timeout to bound
it. Route it through electronNet.fetch (Chromium: OS trust store + system
proxy) with a 10s abort, keeping the warn-and-return-null failure behavior.

apps/server/src already bans bare fetch through a test. Extend the same
enforcement to the two remaining surfaces that talk to the network:

- apps/desktop/electron: unmarked bare fetch fails; the four genuine loopback
  call sites (sidecar health, runtime JSON, daemon shutdown, CDP discovery)
  carry a // loopback-fetch: marker naming why 127.0.0.1 is provable.
- apps/installer/src: bare fetch fails; external requests must use
  fetchWithSystemCa. system-ca.ts (defines the wrapper) and ui-html.ts
  (browser-side template) are exempt; Bun.serve's `async fetch(request)`
  handler is recognized as a definition, not a call.

No configuration, API, or chart surface changes.

* fix(desktop): clarify electron fetch trust guard
…#3131)

* docs(enterprise): document outbound network access and guard it in CI

Enterprise networks block outbound connections by default and IT must approve
each destination, but nothing in this repo listed what OpenWork needs to reach.
Two destinations were named nowhere at all:

- objects.githubusercontent.com, the redirect target of every release-asset
  download. Approving only github.com lets a download start and then fail
  partway through.
- registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds.
  Blocked, the UI-control MCP is silently unavailable while the app still opens.

Add docs/enterprise/outbound-access.md for customer IT, in plain language, with
a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop
client access is documented separately from self-hosted Den access because
different teams approve them on different networks.

Back it with docs/enterprise/outbound-access.json and
scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that
introduces a new destination must document its enterprise impact. The check also
reports manifest entries that no longer exist in code, so the list cannot rot.
Hosts reached indirectly (redirect targets, subprocesses, links, schema strings)
are classified by kind and exempt from the staleness rule.

Docs, one check script, one CI step, one package script. No product behavior,
configuration, API, or chart surface changes.

* docs(enterprise): name the current GitHub release-asset host

Verified against a live OpenWork release asset: github.com now redirects
release downloads to release-assets.githubusercontent.com, not
objects.githubusercontent.com. An allowlist naming only the old host would
still let downloads start and then fail partway through, which is the exact
symptom this page exists to prevent.

List both. release-assets.githubusercontent.com is what GitHub serves today;
objects.githubusercontent.com stays documented for older clients and any
staged rollback.
Den's SSRF guard refuses to fetch private/reserved addresses on behalf of
users, which is correct on hosted multi-tenant deployments. But a common
enterprise topology puts Den inside the customer's private network with their
MCP servers on private addresses, and there the guard blocks every internal
server. The operator sees MCP_URL_BLOCKED and is told to "use a public HTTPS
MCP URL or change the deployment's private-network policy through security
review", with no indication that a supported opt-out exists.

env.ts has provided DEN_ALLOW_PRIVATE_MCP_URLS=1 all along, but it appeared
only in source, unit tests, and one eval flow: not in .env.example, not in the
chart, not in any doc. Surface it in all three places so the fix is reachable
without reading the source.

Scoped to den-api only. den-api, den-web, and inference all envFrom the same
ConfigMap, so adding the key there would inject an SSRF-bypass flag into two
workloads that never read it; the value is emitted directly on the den-api
container instead. Only den-api consumes allowPrivateMcpUrls (mcp-connections,
plugin-system/store, enterprise-mcp-client-adapter, external-mcp-client).

Nothing renders unless an operator opts in, so helm template with default
values is byte-identical to the previous chart: no ConfigMap checksum change
and no pod restart on upgrade. Blank, 0, and false render nothing; only "1"
enables it, matching the trimmed string comparison in env.ts; any other value
fails the render rather than guessing. url-guard.ts and env.ts are untouched.
…#3132)

Settings > Debug runs a bounded initialize -> tools/list check against the
managed openwork-cloud MCP entry. It carries an organization MCP bearer token,
so the probe refuses to send that credential to an origin outside its trust
list. DEFAULT_TRUSTED_ORIGINS holds only the hosted origins, so on every
self-hosted deployment the check returns untrusted_endpoint and never runs.

The escape hatch, OPENWORK_AGENT_DIAGNOSTICS_TRUSTED_ORIGINS, appeared in
exactly two places in the repo: its own source file and its own test. The
guidance an operator actually saw was "have the server administrator explicitly
trust the development origin" -- which calls a production on-prem Den a
development origin and never names the variable, so nobody could act on it.

Name the variable in the operator action, and document it in the self-hosting
guide with the real format rules enforced by configuredTrustedOrigins: bare
origins, comma-separated, https except loopback, invalid entries ignored rather
than failing open, additive to the hosted defaults. The docs state explicitly
that this enables the diagnostic only and that Cloud MCP itself works without
it, so operators do not read a skipped probe as a broken deployment.

The trust model is deliberately unchanged: agent-context-cloud-probe.ts is
untouched, so which origins are trusted cannot differ from before. Trusting the
endpoint's own origin would be circular, since the probe exists to validate the
very config that names it. Tests pin that an unlisted self-hosted origin still
performs no fetch, and that the action keeps naming the variable.
…yment (#3135)

Most enterprise installs are not fully air-gapped. The common shape is laptops
with internet plus VPN, and Den web and Den API inside the customer's private
network. That split was undocumented, so every customer rediscovered its
gotchas by hitting failures.

Add a published page covering the two network paths separately, because
different teams approve them: what the desktop must reach, what Den must reach
(only MySQL and ghcr.io at pull time are hard requirements), and the three
things that surprise operators, each with symptom, cause, and fix:

- Internal MCP servers are refused by the SSRF guard until
  DEN_ALLOW_PRIVATE_MCP_URLS=1, with the security tradeoff stated.
- The Settings > Debug cloud catalog probe is skipped because only hosted
  origins are trusted. It states explicitly that this affects the diagnostic
  only and Cloud MCP still works, so a skipped probe is not read as an outage,
  and that the variable must be set before launch because the in-app
  environment store strips OPENWORK_* keys.
- Den's outbound self-check targets a public host by default and can be
  self-hosted with DEN_DIAGNOSTICS_ORIGIN.

Also resolve a contradiction that decided whether IT opens one hostname or
two. docs/org-install-links.md claimed "the desktop must be able to reach both
origins"; resolveDenBaseUrls ignores an explicit API origin when a base URL is
present and derives <baseUrl>/api/den, proven by the existing den-mcp-url test.
Steady-state desktop traffic needs one origin. A separate API origin is only
reachable-critical for the one-time install-link exchange, which builds its
endpoint from the apiBaseUrl carried in the link, and for external MCP clients.
Both docs now state their context instead of disagreeing.
Most enterprise installs are not fully air-gapped: laptops have internet plus
VPN while Den web and Den API sit inside the customer's private network. That
shape depends on four guarantees that no test held, so a refactor could change
on-prem behavior, or an enterprise firewall requirement, without anything
turning red.

Add an internal flow with four frames, each backed by real evidence rather
than prose:

1. resolveDenBaseUrls ignores an explicitly configured API origin when a base
   URL is present, so the desktop keeps needing exactly one Den hostname.
2. url-guard refuses private and reserved MCP addresses by default, and
   DEN_ALLOW_PRIVATE_MCP_URLS is the only documented way in.
3. The cloud catalog probe returns untrusted_endpoint and performs no request
   for an untrusted origin, so a self-hosted deployment leaks no organization
   MCP token and the diagnostic is skipped rather than failed.
4. DEN_DIAGNOSTICS_ORIGIN selects the outbound self-check target, with the
   public host as an explicit default, and the bearer token minimum of 24
   characters is enforced.

Runs with no app, no database, and no network: requiresApp is false and it
passes against a dead CDP URL. Evidence comes from focused Bun test filters
plus direct imports of the pure functions.

Verified it can fail: breaking resolveDenBaseUrls fails frame 1 and forcing
allowPrivateMcpUrls true fails frame 2, each naming the broken property.
…ce (#3139)

* docs(enterprise): document outbound network access and guard it in CI

Enterprise networks block outbound connections by default and IT must approve
each destination, but nothing in this repo listed what OpenWork needs to reach.
Two destinations were named nowhere at all:

- objects.githubusercontent.com, the redirect target of every release-asset
  download. Approving only github.com lets a download start and then fail
  partway through.
- registry.npmjs.org, used by `npx -y openwork-ui-mcp` in packaged builds.
  Blocked, the UI-control MCP is silently unavailable while the app still opens.

Add docs/enterprise/outbound-access.md for customer IT, in plain language, with
a copy-pasteable minimum allowlist and a what-breaks-when-blocked column. Desktop
client access is documented separately from self-hosted Den access because
different teams approve them on different networks.

Back it with docs/enterprise/outbound-access.json and
scripts/check-outbound-access.mjs, wired into ci-tests.yml, so a feature that
introduces a new destination must document its enterprise impact. The check also
reports manifest entries that no longer exist in code, so the list cannot rot.
Hosts reached indirectly (redirect targets, subprocesses, links, schema strings)
are classified by kind and exempt from the staleness rule.

Docs, one check script, one CI step, one package script. No product behavior,
configuration, API, or chart surface changes.

* docs(self-host): make the cloud catalog diagnostic actionable on-prem

Settings > Debug runs a bounded initialize -> tools/list check against the
managed openwork-cloud MCP entry. It carries an organization MCP bearer token,
so the probe refuses to send that credential to an origin outside its trust
list. DEFAULT_TRUSTED_ORIGINS holds only the hosted origins, so on every
self-hosted deployment the check returns untrusted_endpoint and never runs.

The escape hatch, OPENWORK_AGENT_DIAGNOSTICS_TRUSTED_ORIGINS, appeared in
exactly two places in the repo: its own source file and its own test. The
guidance an operator actually saw was "have the server administrator explicitly
trust the development origin" -- which calls a production on-prem Den a
development origin and never names the variable, so nobody could act on it.

Name the variable in the operator action, and document it in the self-hosting
guide with the real format rules enforced by configuredTrustedOrigins: bare
origins, comma-separated, https except loopback, invalid entries ignored rather
than failing open, additive to the hosted defaults. The docs state explicitly
that this enables the diagnostic only and that Cloud MCP itself works without
it, so operators do not read a skipped probe as a broken deployment.

The trust model is deliberately unchanged: agent-context-cloud-probe.ts is
untouched, so which origins are trusted cannot differ from before. Trusting the
endpoint's own origin would be circular, since the probe exists to validate the
very config that names it. Tests pin that an unlisted self-hosted origin still
performs no fetch, and that the action keeps naming the variable.

* docs(helm): expose the private-network MCP opt-out to operators

Den's SSRF guard refuses to fetch private/reserved addresses on behalf of
users, which is correct on hosted multi-tenant deployments. But a common
enterprise topology puts Den inside the customer's private network with their
MCP servers on private addresses, and there the guard blocks every internal
server. The operator sees MCP_URL_BLOCKED and is told to "use a public HTTPS
MCP URL or change the deployment's private-network policy through security
review", with no indication that a supported opt-out exists.

env.ts has provided DEN_ALLOW_PRIVATE_MCP_URLS=1 all along, but it appeared
only in source, unit tests, and one eval flow: not in .env.example, not in the
chart, not in any doc. Surface it in all three places so the fix is reachable
without reading the source.

Scoped to den-api only. den-api, den-web, and inference all envFrom the same
ConfigMap, so adding the key there would inject an SSRF-bypass flag into two
workloads that never read it; the value is emitted directly on the den-api
container instead. Only den-api consumes allowPrivateMcpUrls (mcp-connections,
plugin-system/store, enterprise-mcp-client-adapter, external-mcp-client).

Nothing renders unless an operator opts in, so helm template with default
values is byte-identical to the previous chart: no ConfigMap checksum change
and no pod restart on upgrade. Blank, 0, and false render nothing; only "1"
enables it, matching the trimmed string comparison in env.ts; any other value
fails the render rather than guessing. url-guard.ts and env.ts are untouched.

* docs(enterprise): name the current GitHub release-asset host

Verified against a live OpenWork release asset: github.com now redirects
release downloads to release-assets.githubusercontent.com, not
objects.githubusercontent.com. An allowlist naming only the old host would
still let downloads start and then fail partway through, which is the exact
symptom this page exists to prevent.

List both. release-assets.githubusercontent.com is what GitHub serves today;
objects.githubusercontent.com stays documented for older clients and any
staged rollback.

* docs(self-host): document the private-network (semi air-gapped) deployment

Most enterprise installs are not fully air-gapped. The common shape is laptops
with internet plus VPN, and Den web and Den API inside the customer's private
network. That split was undocumented, so every customer rediscovered its
gotchas by hitting failures.

Add a published page covering the two network paths separately, because
different teams approve them: what the desktop must reach, what Den must reach
(only MySQL and ghcr.io at pull time are hard requirements), and the three
things that surprise operators, each with symptom, cause, and fix:

- Internal MCP servers are refused by the SSRF guard until
  DEN_ALLOW_PRIVATE_MCP_URLS=1, with the security tradeoff stated.
- The Settings > Debug cloud catalog probe is skipped because only hosted
  origins are trusted. It states explicitly that this affects the diagnostic
  only and Cloud MCP still works, so a skipped probe is not read as an outage,
  and that the variable must be set before launch because the in-app
  environment store strips OPENWORK_* keys.
- Den's outbound self-check targets a public host by default and can be
  self-hosted with DEN_DIAGNOSTICS_ORIGIN.

Also resolve a contradiction that decided whether IT opens one hostname or
two. docs/org-install-links.md claimed "the desktop must be able to reach both
origins"; resolveDenBaseUrls ignores an explicit API origin when a base URL is
present and derives <baseUrl>/api/den, proven by the existing den-mcp-url test.
Steady-state desktop traffic needs one origin. A separate API origin is only
reachable-critical for the one-time install-link exchange, which builds its
endpoint from the apiBaseUrl carried in the link, and for external MCP clients.
Both docs now state their context instead of disagreeing.

* docs(self-host): consolidate network, air-gap, and certificate guidance

Promote Self-host from a small Start here group to a dedicated docs tab while
preserving every existing start-here URL. Consolidate scattered network
material into five focused published pages: air-gapped deployment, outbound
network access, certificate trust and proxies, installer delivery, and network
diagnostics. Keep the private-network topology page as the high-level entry.

Make boundaries explicit instead of conflating them:

- mounted installers solve installer delivery, not full product isolation;
- desktop Chromium, spawned sidecars, Den containers, and MySQL use different
  trust surfaces;
- Linux Electron may require NSS/user trust even when /etc/ssl is correct;
- sslmode=require encrypts but is not certificate identity verification;
- a skipped Cloud catalog diagnostic does not mean Cloud MCP is broken.

Promote the machine-guarded outbound manifest into customer-facing docs,
retain repository operator pages and their existing anchors, and slim the
self-host/private-network overviews so there is one canonical owner per detail.
The Helm and diagnostics READMEs remain authoritative for low-level mechanics.

This branch is stacked on PRs #3131, #3132, #3133, and #3135. It changes docs
and navigation only; no product, Helm template, environment, skill, or eval
behavior changes.
* test(den): add complete skill CRUD proof flow

* fix(evals): support native MySQL in Daytona
dependabot Bot and others added 30 commits July 31, 2026 02:42
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.21.0 to 4.23.1.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](privatenumber/tsx@v4.21.0...v4.23.1)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@codemirror/lang-markdown](https://github.com/codemirror/lang-markdown) from 6.5.0 to 6.5.1.
- [Changelog](https://github.com/codemirror/lang-markdown/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/lang-markdown/commits)

---
updated-dependencies:
- dependency-name: "@codemirror/lang-markdown"
  dependency-version: 6.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* build(deps): bump next from 16.2.1 to 16.2.11

Bumps [next](https://github.com/vercel/next.js) from 16.2.1 to 16.2.11.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](vercel/next.js@v16.2.1...v16.2.11)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: pin next browser mapping transitive

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: OmarMcAdam <gh@mcadam.io>
* build(deps): bump react-dom and @types/react-dom

Bumps [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

Updates `react-dom` from 18.2.0 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `@types/react-dom` from 18.2.25 to 19.2.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.3
  dependency-type: direct:development
  update-type: version-update:semver-major
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: align react catalog with react dom

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: OmarMcAdam <gh@mcadam.io>
…3376)

The boot takeover showed a progress bar hardcoded to two thirds that only
pulsed, so a slow sandbox read as frozen at 66%. It also ran two spinners
at once, and Retry appeared only after a hard failure, leaving a slow but
healthy boot with nothing to do.

Progress is now derived from the status we already poll, so it can never
claim more than we can prove: a provisioning sandbox is still being
reserved, while a waking or updating one demonstrably exists. Only the
active checkpoint animates. At 45s the copy escalates and Retry / Sign out
appear.

Also fixes the dot ticker, which computed to fully transparent: the
--dls-*-rgb tokens are space-separated but the rules used legacy comma
rgba(), so the indicator was invisible everywhere it was used, including
the composer wait state.

Co-authored-by: Cursor <cursoragent@cursor.com>
…3375)

Providers were identifiable only by a two-letter monogram unless they were
one of the three brands with a bundled inline SVG, which made the connect
dialog read as a wall of grey initials.

Adds a logo resolution ladder (inline SVG, Simple Icons, favicon, monogram
last) so a provider only falls back to initials once every source is
exhausted. Long-tail catalog ids are slugified domains, so unslugging
"302ai" to "302.ai" recovers a real mark instead of "30".

Builds on that to group the dialog into Connected and All providers, lead
the API-key step with the provider's own logo, and make the model picker's
API-keys row reflect whether any keys exist yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
better-sqlite3 11.10.0 ships no `node-v137` (Node 24) prebuild, so every CI
install fell through `prebuild-install` to `node-gyp rebuild`, which downloads
the Node headers from nodejs.org mid-install. When that request stalls the whole
install dies:

    prebuild-install warn install No prebuilt binaries found (target=24.13.0 ...)
    gyp ERR! stack AggregateError [ETIMEDOUT]
    [ELIFECYCLE] Command failed with exit code 1

Bump better-sqlite3 to ^12.11.1, which publishes node-v137 (and v127/v141/v147)
prebuilds, so installs fetch a binary instead of compiling. 12.0.0 was a
build-matrix-only major (dropped EOL Node 18 / Electron 26-28); the JS API is
unchanged and Electron 35 (ABI 133) prebuilds are still published.

Also cache the pnpm store in the tests workflow, matching the release/alpha
workflows: OpenWork Tests was re-downloading all 1453 packages every run
("reused 0"), which is both slow and extra network surface to flake on.
…ehind its components, timeline observability (#3359)

* feat(evals): timeline primitive, workspace seeding, and honest visual expectations

* fix(evals): seed completes onboarding and engine boot; wait on product state not route shapes

* fix(evals): keep app-smoke on the product's own workspace creation path

* test(evals): app pointed at a TLS-broken den must name the fault (welcome surface, no onboarding needed)

* chore(evals): single-spec sandbox runner

* test(evals): adapt the TLS-fault spec to the real pre-workspace surface

* test(evals): choose the sign-in control from real buttons

* test(evals): TLS-intercepted den — no false 'connected', and diagnostics name the interception

* test(evals): one frame — sign-in against a broken den changes nothing visually

* test(evals): name the TLS spec after what it actually proves

* fix(evals): short per-probe timeouts so busy renderers are retried, not waited on

* fix(evals): read the created workspace via the proven in-page server call, not an unverified bridge channel

* test(evals): poll patiently through the cold-profile renderer block

* fix(evals): poll for the created workspace record through renderer blocks

* fix(evals): resolve the workspace id from product state after onboarding instead of a hanging server fetch

* fix(evals): idempotent reads retry through renderer blocks

* fix(evals): retry reads to a deadline, not a fixed attempt count

* fix(evals): re-attach to the app's current page target when evaluations stop answering

* fix(evals): background prep does not make the welcome surface un-interactive

* chore(evals): verify the X display actually answers before spawning Electron

* chore(evals): clear stale Electron processes before each sandbox spec run

* fix(evals): resolve the workspace id after onboarding completes, not before

* fix(evals): report the workspace id even while the welcome surface is showing

* test(evals): assert the first-run workspace id after onboarding adopts it

* test(evals): use the resolved workspace id; report loaded skills on failure

* fix(evals): parse the skill name from the row, and drop a regex that never compiled

* test(evals): wait for the assistant to settle; use the session control when the button is not text

* test(evals): wait for output to stabilise; stop assuming session ids appear in the route

* fix(evals): settings and extensions surfaces are interactive too

* test(evals): read the created session id from the product's session list

* test(evals): drop an exact timing threshold that can flake by a millisecond

* test(evals): first run signs in through a real browser, then shares a skill via a marketplace

* chore(evals): verify the display in the full runner; give single-spec runs den env

* refactor(evals): move environment healing behind the components that own it

Target re-attach now lives in @openwork/cdp (evaluateOnSurface), display liveness
and stale-surface cleanup in @openwork/hosts (spawnElectron), and resolveHost no
longer picks the Daytona host when the caller is already inside a sandbox. The
sandbox runner scripts drop the logic they were carrying.

* test(evals): match the real desktop-auth handoff URL and read Den's deep link from the browser

* test(evals): switch the browser from sign-up to sign-in for a seeded account

* chore(evals): let a single-spec run ensure the Den stack when the spec needs it

* test(evals): collapse workspace arrangement to the proven onboarding path

seedWorkspace arranged a workspace over the local server API and left the
engine unconfigured and the model catalog empty — a state the product's own
onboarding never produces, so specs failed on arrangement instead of their
subject. Every spec now goes through createAndSelectWorkspace, the path the
passing journeys already proved.

Also read the created session id from the field session.list_sessions really
returns: items carry sessionId, not id (probed live; the old extraction never
matched, so the wait timed out even with sessions present).

* test(evals): drive den web's real two-step sign-in and read the sign-in code input

Probed live on a running den: auth is an email-only step (Next) that routes
to a password step (Sign in) — there is no single form and no sign-up/sign-in
toggle. After sign-in the deep link is not an anchor or page text; it is a
readonly input holding the openwork://den-auth?grant=… sign-in code next to
Open OpenWork, so read it from there.

* fix(evals): keep pnpm version redirection warm inside isolated Electron profiles

Each surface's fresh HOME hides the host's pnpm tool cache, so the pinned
pnpm (packageManager) is re-downloaded from the network on every spawn
(21MB). On the eval sandbox one failed download degenerated into a
self-sustaining recursive 'pnpm add pnpm' cascade that outlived the run and
starved later spawns. Point PNPM_HOME at the host's real pnpm home; the
redirect is then local and instant (verified: fresh HOME resolves in 0.75s
with no downloads).

* chore(evals): export den eval env only when a den actually answers

Exporting OPENWORK_EVAL_DEN_API_URL unconditionally made den-gated specs run
against nothing and die in 3ms with 'fetch failed' instead of skipping with
their own reason. Probe /health first; with OPENWORK_SPEC_NEEDS_DEN=1 the
stack is still ensured before the probe.

* test(evals): scroll the claimed skill row into view before its screenshot

The skills menu is a scrollable list: /browser-automation was loaded (the
DOM assertions passed) while the screenshot showed only the rows above it,
so the visual validator honestly reported it absent.

* fix(evals): survive renderer freeze bursts while reading the skills menu

The workspace engine boot blocks the renderer JS thread in bursts. Bare
20s evaluations (plug-menu clicks) died with raw CDP timeouts, and the
measure evaluations gave their outer CDP call the same 20s deadline as the
in-page poll, so the two raced under load. Fold the menu clicks into a
guarded, retried wait and give the measures headroom over their inner
deadlines.

* test(evals): reveal the menu row a visual claim names before its screenshot

The capability menu is a scrollable popover: all four sections were in the
DOM (the code assertion passed) while Agents sat above the fold, so the
validator honestly reported it invisible. Scroll the named row into view and
let the code assertion carry section completeness.

* test(evals): assert the capability menu visuals a person can actually see

The vision loop caught a real product defect: with a long command list the
capability popover extends under the window header, so its first section row
(Agents) is covered and unclickable (repo workspace, popover top at y=17
under the title bar). Keep four-section completeness as the DOM assertion,
scope the visual claim to the visible sections, and record the defect where
the claim is made.

* test(evals): arm the slow-cloud scenario separately and drive the menu with retried waits

The scenario opened the plug menu inside its own evaluate with fixed
100/300ms delays right after a reload; under load those clicks land before
React handlers attach and the whole block times out. Arm the fetch delay and
the connect witness in one small mutation, open the menu with the guarded
retried helper, then measure.

* test(evals): arrange the real cloud-connected desktop state for slow-cloud skills

The scenario set window.__OPENWORK_GATEWAY__, which flips the app into the
hosted gateway runtime and rewires the local openwork-server client to the
page origin — so client.listSkills hit the Vite dev server and local skills
never rendered, a state no desktop user can reach. The den auth token and
active org id it also set are the real cloud-connected desktop arrangement,
so keep only those. Drop the unused resetSkillsCloudState.

* test(evals): drop the slow-cloud skills block whose arrangement cannot be faithful

On desktop, den traffic goes through the main process, so the renderer
fetch hook never delayed anything (denRequestCount stayed 0), and the
gateway marker it used instead rewired the local server to the page origin.
The block therefore only ever measured an impossible state. Cloud latency
belongs in a den-boundary fault spec like app-den-tls-fault, where the
fault is injected for real.
#3387)

* feat(evals): timeline primitive, workspace seeding, and honest visual expectations

* fix(evals): seed completes onboarding and engine boot; wait on product state not route shapes

* fix(evals): keep app-smoke on the product's own workspace creation path

* test(evals): app pointed at a TLS-broken den must name the fault (welcome surface, no onboarding needed)

* chore(evals): single-spec sandbox runner

* test(evals): adapt the TLS-fault spec to the real pre-workspace surface

* test(evals): choose the sign-in control from real buttons

* test(evals): TLS-intercepted den — no false 'connected', and diagnostics name the interception

* test(evals): one frame — sign-in against a broken den changes nothing visually

* test(evals): name the TLS spec after what it actually proves

* fix(evals): short per-probe timeouts so busy renderers are retried, not waited on

* fix(evals): read the created workspace via the proven in-page server call, not an unverified bridge channel

* test(evals): poll patiently through the cold-profile renderer block

* fix(evals): poll for the created workspace record through renderer blocks

* fix(evals): resolve the workspace id from product state after onboarding instead of a hanging server fetch

* fix(evals): idempotent reads retry through renderer blocks

* fix(evals): retry reads to a deadline, not a fixed attempt count

* fix(evals): re-attach to the app's current page target when evaluations stop answering

* fix(evals): background prep does not make the welcome surface un-interactive

* chore(evals): verify the X display actually answers before spawning Electron

* chore(evals): clear stale Electron processes before each sandbox spec run

* fix(evals): resolve the workspace id after onboarding completes, not before

* fix(evals): report the workspace id even while the welcome surface is showing

* test(evals): assert the first-run workspace id after onboarding adopts it

* test(evals): use the resolved workspace id; report loaded skills on failure

* fix(evals): parse the skill name from the row, and drop a regex that never compiled

* test(evals): wait for the assistant to settle; use the session control when the button is not text

* test(evals): wait for output to stabilise; stop assuming session ids appear in the route

* fix(evals): settings and extensions surfaces are interactive too

* test(evals): read the created session id from the product's session list

* test(evals): drop an exact timing threshold that can flake by a millisecond

* test(evals): first run signs in through a real browser, then shares a skill via a marketplace

* chore(evals): verify the display in the full runner; give single-spec runs den env

* refactor(evals): move environment healing behind the components that own it

Target re-attach now lives in @openwork/cdp (evaluateOnSurface), display liveness
and stale-surface cleanup in @openwork/hosts (spawnElectron), and resolveHost no
longer picks the Daytona host when the caller is already inside a sandbox. The
sandbox runner scripts drop the logic they were carrying.

* test(evals): match the real desktop-auth handoff URL and read Den's deep link from the browser

* test(evals): switch the browser from sign-up to sign-in for a seeded account

* chore(evals): let a single-spec run ensure the Den stack when the spec needs it

* test(evals): collapse workspace arrangement to the proven onboarding path

seedWorkspace arranged a workspace over the local server API and left the
engine unconfigured and the model catalog empty — a state the product's own
onboarding never produces, so specs failed on arrangement instead of their
subject. Every spec now goes through createAndSelectWorkspace, the path the
passing journeys already proved.

Also read the created session id from the field session.list_sessions really
returns: items carry sessionId, not id (probed live; the old extraction never
matched, so the wait timed out even with sessions present).

* test(evals): drive den web's real two-step sign-in and read the sign-in code input

Probed live on a running den: auth is an email-only step (Next) that routes
to a password step (Sign in) — there is no single form and no sign-up/sign-in
toggle. After sign-in the deep link is not an anchor or page text; it is a
readonly input holding the openwork://den-auth?grant=… sign-in code next to
Open OpenWork, so read it from there.

* fix(evals): keep pnpm version redirection warm inside isolated Electron profiles

Each surface's fresh HOME hides the host's pnpm tool cache, so the pinned
pnpm (packageManager) is re-downloaded from the network on every spawn
(21MB). On the eval sandbox one failed download degenerated into a
self-sustaining recursive 'pnpm add pnpm' cascade that outlived the run and
starved later spawns. Point PNPM_HOME at the host's real pnpm home; the
redirect is then local and instant (verified: fresh HOME resolves in 0.75s
with no downloads).

* chore(evals): export den eval env only when a den actually answers

Exporting OPENWORK_EVAL_DEN_API_URL unconditionally made den-gated specs run
against nothing and die in 3ms with 'fetch failed' instead of skipping with
their own reason. Probe /health first; with OPENWORK_SPEC_NEEDS_DEN=1 the
stack is still ensured before the probe.

* test(evals): scroll the claimed skill row into view before its screenshot

The skills menu is a scrollable list: /browser-automation was loaded (the
DOM assertions passed) while the screenshot showed only the rows above it,
so the visual validator honestly reported it absent.

* fix(evals): survive renderer freeze bursts while reading the skills menu

The workspace engine boot blocks the renderer JS thread in bursts. Bare
20s evaluations (plug-menu clicks) died with raw CDP timeouts, and the
measure evaluations gave their outer CDP call the same 20s deadline as the
in-page poll, so the two raced under load. Fold the menu clicks into a
guarded, retried wait and give the measures headroom over their inner
deadlines.

* test(evals): reveal the menu row a visual claim names before its screenshot

The capability menu is a scrollable popover: all four sections were in the
DOM (the code assertion passed) while Agents sat above the fold, so the
validator honestly reported it invisible. Scroll the named row into view and
let the code assertion carry section completeness.

* test(evals): assert the capability menu visuals a person can actually see

The vision loop caught a real product defect: with a long command list the
capability popover extends under the window header, so its first section row
(Agents) is covered and unclickable (repo workspace, popover top at y=17
under the title bar). Keep four-section completeness as the DOM assertion,
scope the visual claim to the visible sections, and record the defect where
the claim is made.

* test(evals): arm the slow-cloud scenario separately and drive the menu with retried waits

The scenario opened the plug menu inside its own evaluate with fixed
100/300ms delays right after a reload; under load those clicks land before
React handlers attach and the whole block times out. Arm the fetch delay and
the connect witness in one small mutation, open the menu with the guarded
retried helper, then measure.

* test(evals): arrange the real cloud-connected desktop state for slow-cloud skills

The scenario set window.__OPENWORK_GATEWAY__, which flips the app into the
hosted gateway runtime and rewires the local openwork-server client to the
page origin — so client.listSkills hit the Vite dev server and local skills
never rendered, a state no desktop user can reach. The den auth token and
active org id it also set are the real cloud-connected desktop arrangement,
so keep only those. Drop the unused resetSkillsCloudState.

* test(evals): drop the slow-cloud skills block whose arrangement cannot be faithful

On desktop, den traffic goes through the main process, so the renderer
fetch hook never delayed anything (denRequestCount stayed 0), and the
gateway marker it used instead rewired the local server to the page origin.
The block therefore only ever measured an impossible state. Cloud latency
belongs in a den-boundary fault spec like app-den-tls-fault, where the
fault is injected for real.

* test(evals): wait for the sign-in outcome, and give org specs a workspace before sign-in

signInInBrowser returned right after clicking Sign in, so the next frame
honestly showed 'Working...'. Wait for the signed-in outcome (or the
sign-in code) before returning.

The signed-in org shell offers no Add workspace entry, and the
workspace.create control there reports ok while creating nothing (its error
lands in closed-modal state) — probed live twice. Arrange org specs the way
a member actually gets there: create the workspace on the proven welcome
path first, then sign in; organization onboarding is completed when it
appears and the existing workspace is reselected either way.

* test(evals): send the role the marketplace access grant API requires

POST /v1/marketplaces/:id/access rejects a grant without role
(viewer|editor|manager). Viewer is what sharing with a colleague means.

* test(evals): read shared skill names from the plugin's resolved components

The resolved marketplace payload carries plugins with component counts only;
skill names live on GET /v1/plugins/:id/resolved as items[].configObject
(objectType skill, title). Read them as the member so visibility stays the
colleague's, not the admin's.

* test(evals): poll the model catalog past its first paint, and pin the managed-recovery defect

The picker can paint 'No models' before the engine's catalog lands (observed
live: 0 models on first read, 7 shortly after on the same boot), so the
primary test polls up to 90s before asserting.

The managed test now pins today's truth: after an admin publishes a
provider, Retry does not deliver it to the composer even though the API
already entitles the member (probed live, 201 + readable as the member, empty
notice survives 90s+). The spec asserts the notice stays honest and the
composer stays usable; when live recovery ships, the pinned wait fails
loudly and the recovery assertions come back from history.

* test(evals): pin the managed defect without typing (no editor without a model)

With no selectable model the composer renders no contenteditable editor, so
the pinned block asserts what is really there: the persistent notice with
Retry and no crash.

* test(evals): pin only the persistent notice after Retry (the button reads Refreshing mid-flight)

* feat(evals): make surface placement explicit, and own browser disposal

resolveHost() reads one ambient env var, so a whole process got one host:
everything local, or everything in one sandbox. A spec could not say 'app
here, browser there' or 'two desktops in two sandboxes' — the topology was
inherited from the environment instead of declared.

- desktop({ host }) and the new chrome({ host }) take a placement.
- localHost() / daytonaSandbox(id) name one, with REPO_ROOT wiring kept
  inside the package. daytonaSandbox() fails loudly from inside a sandbox,
  where the CLI indirection has nothing to target.
- resolveHost() default behaviour is unchanged, so existing specs are
  untouched.

chrome() also fixes a real leak: attachSurface only closes the CDP socket, so
the hand-rolled spawnChrome + attachSurface in first-run-cloud-share left a
browser process running after every run. Disposing now stops the client and
then disposes the surface, which is what kills it.
* feat(markdown): render LaTeX math in chat with KaTeX

OpenWork rendered Markdown but had no math engine, so $...$, $$...$$,
\(...\) and \[...\] all showed up as raw LaTeX source in technical
conversations (reported against v0.18.12).

- Add a markdown-math marked extension: marked-katex-extension covers the
  standard dollar delimiters (same approach as upstream opencode web), plus
  local block/inline extensions for the \(...\) and \[...\] pairs models
  commonly emit, which markdown would otherwise swallow as escapes.
- Register it on both the sync and Shiki-highlighted parsers so formulas do
  not flicker when a message with a code block upgrades.
- Allow <semantics>/<annotation> through DOMPurify; they are not in its
  default MathML allowlist, so the accessible MathML branch (and copy-as-TeX)
  was being stripped.
- Wrap every math renderer so malformed LaTeX degrades to visible source
  instead of throwing out of the React render and blanking the message.
- Skip .katex when applying find-in-conversation highlights.
- Import katex CSS and keep formulas at the surrounding text size.

Tests: apps/app/tests/markdown-math.test.ts covers all four delimiters,
MathML survival through sanitization, graceful failure, currency amounts,
code spans, both parsers and both presentations.

* test(evals): stage the math proof seed so each frame shows new content

* test(evals): match inline-stage formula count

* Potential fix for pull request finding 'CodeQL / Improper code sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…box-aware ports, profile pruning (#3388)

* fix(evals): ask the host for its workspace root instead of assuming the driver's

A spec passing process.cwd() as a workspace path is only correct when the
driver and the app share a filesystem. Driving a sandbox from a laptop, the
app was asked to open a directory that exists only on the laptop — observed
as onboarding hanging 120s on 'Power your first task' with no error, which
reads like a broken app rather than a bad path.

Host.workspaceRoot states it (repoRoot locally, /workspace on Daytona) and
DesktopHandle re-exposes it, so app-smoke asks the app's own host.

* fix(evals): allocate Daytona surface ports that are free INSIDE the sandbox

The local host finds a free port by binding one; the Daytona host cannot,
because the port must be free on another machine. It allocated from a local
counter instead, so the OpenCode sidecar — which picks its own port at boot —
was observed holding 9825, the CDP primary. Electron's debugger never bound
and the preview URL timed out after 180s with nothing naming the cause.

Ask the sandbox which ports are listening and skip those.

* fix(evals): app-smoke opens a fresh small folder, not the repo root

Routing app-smoke through Host.workspaceRoot made it open the whole monorepo
(/workspace, node_modules included). The engine scan then blocked the renderer
past 240s and the spec failed reading location.hash — a regression versus the
old process.cwd() (which happened to be the smaller /workspace/evals).

workspaceRoot stays the right primitive for specs that genuinely need the repo
(skills-local reads .opencode/skills); a workspace path just needs to be small
and valid on the app's host.

* fix(evals): prune stale surface profiles, not just their processes

Disposal removes a profile on the happy path, but every killed or failed run
leaks one at ~50MB. Twenty-seven filled a 10GB sandbox to 99%, and the symptom
was not 'disk full' — it was the renderer failing to answer Runtime.evaluate
for 240s, indistinguishable from a broken app. Cost four debug cycles to find.

clearStaleSurfaces already pkills by rootDir and so already assumes exclusive
ownership; deleting the directories it just orphaned is the matching half.

* test(evals): two members each connect their own account to one org connector and call its tools

Two SEPARATE desktops, own profiles, signed in as different members. A single
desktop clearing localStorage only simulates per-member isolation; two make it
real — the load-bearing assertion is that member B still needs their own
sign-in after member A connected.

The connector is the authority on use: mock MCP now records tool ARGUMENTS and
a sha256 fingerprint of the caller's bearer token, so the spec asserts on calls
actually served and on DISTINCT credentials rather than the app's own
'Connected' text.

Placement: both desktops use the ambient host; moving either to its own sandbox
is desktop({ host: daytonaSandbox(id) }) once mode B is green.

* fix(evals): email verification is best effort; the sign-in that follows is the test

ensureMemberSession threw when mark-verified failed, even though the member had
just been created successfully and could sign in. On the eval sandbox the helper
uses -uroot with no password while den's MariaDB root has one, so bootstrapping
a second member aborted the whole spec at 2s.

Warn and continue; if the subsequent sign-in fails, report both causes.

* test(evals): wait for the connections surface to settle, and refresh the way the proven spec does

* test(evals): name what was on screen when a connection card never renders

* test(evals): steer back to settings when the app opens a new session mid-poll

The card poll was watching the session surface: the app opens a freshly created
session on its own and navigates away from settings. Re-navigate each iteration
instead of assuming the route stays put.

* test(evals): short, failure-tolerant probes while two desktops contend

Two desktops in one sandbox make the renderer freeze in bursts; a bare 20s
Runtime.evaluate turns one freeze into a failed spec. Probe briefly and retry,
which is what the polling loop is for.

* test(evals): gate the connector spec on the blank-connections defect

The org connections surface renders empty for a signed-in org member on current
dev — correct route, empty body text. org-connection-lifecycle (untouched by
this branch) independently regressed the same way today after the sandbox pulled
merged dev, which includes #3375 across the connect flow.

Everything before that surface is verified. Opt-in via
OPENWORK_EVAL_CONNECTOR_SPEC=1 so a product defect cannot masquerade as a
nightly failure; remove the gate when the surface renders.

* test(evals): match the canonical extensions route the app rewrites to

The app canonicalises /workspace/<id>/settings/extensions/connections to
/workspace/<id>/extensions/connections. Checking for the pre-rewrite form made
the steer-back re-navigate every iteration, fighting the rewrite so the surface
never settled — which presented as a blank page and which I wrongly attributed
to a product regression. A probe found no render error and 31 buttons on the
canonical route.

* fix(evals): name the expression when a CDP evaluation times out

The rejection fires from a timer, so the caller's stack is already gone:
'CDP call Runtime.evaluate timed out after 20000ms' cannot say which of a
spec's dozens of evaluations blocked. Include a truncated expression.

* fix(evals): retry navigation through renderer freeze bursts

go() set the hash with a bare 20s evaluate. Under contention (two desktops on
one host) a freeze burst killed the whole journey at its last phase. Setting a
hash is idempotent, so retry it.

* test(evals): frame the detail panel once it has painted, not once its text is in the DOM

waitForText proves the DOM; a frame proves pixels. The detail panel paints
slightly after its text lands, so the vision check intermittently caught a blank
panel — it passed one run and failed the next. Wait for a laid-out node, scroll
it into view, allow a paint.

* test(evals): match rendered text when revealing, not raw textContent

waitForText reads innerText, which is render-aware: a badge styled with CSS
text-transform reads NEEDS YOUR SIGN-IN on screen while textContent holds
'Needs your sign-in'. The reveal could never agree with the wait.

* fix(evals): give navigation a budget that survives a starved renderer

* test(evals): settle canonical connection lifecycle frames

* test(evals): avoid duplicate lifecycle proof frame

* test(evals): prove two explicit Daytona desktop placements

* test(evals): deduplicate disconnected lifecycle frame

* fix(evals): make an externally-managed mock connector usable across runs

publicUrl was a dead seam: waitForHealth read child?.exitCode on the null
child and declared the (never-spawned) server exited, and the request log
of a long-lived mock satisfied atLeast with a PREVIOUS run's tool calls,
returning before this run's calls arrived. Guard the health check to only
fail on a spawned child that exited, carry the mock's timestamp on each
tool call, and let callers scope toolCalls with sinceIso.

* test(evals): place each connector member's desktop on its own Daytona sandbox

Two desktops plus two engines starve renderers on one 9GB sandbox — that
ceiling, not any product defect, is what kept the tool-call phase red, so
the header's retracted defect story goes too. OPENWORK_EVAL_DAYTONA_SANDBOX_A/_B
now place each member via desktop({ host: daytonaSandbox(id) }), the mock
publishes through OPENWORK_EVAL_CONNECTOR_MOCK_PUBLIC_URL so Den, both
desktop browsers, and the driver can all reach it, and this run's tool
calls are scoped with sinceIso.

Green twice end to end (133.0s, 135.2s): both members OAuth-connect their
own account, call mock_echo from the composer, and the connector witnesses
two distinct bearer fingerprints.

* fix(evals): parse the mock's bearer header in linear time

CodeQL: js/polynomial-redos — ^Bearer\s+(.+)$ backtracks polynomially on
'Bearer' followed by header spam. One shared linear parse for the auth
check and the fingerprint.
* ci: add Warden security clearance for PR diffs

Warden runs a single custom skill (diff-security-review) on every PR to
dev and answers one question: does this diff introduce a new security
issue? A workflow_run gatekeeper grants a 'security clearance' review
when the diff is clean, satisfying the required-review gate in the
branch ruleset. Humans still review and merge.

- Clearance identity is a dedicated GitHub App that never pushes, so
  require_last_push_approval holds by construction.
- The gatekeeper executes from the default branch and refuses to clear
  PRs touching .github/, warden.toml, .warden/, or agent skill dirs.
- dismiss_stale_reviews_on_push revokes clearance on every new push;
  Warden re-runs and re-clears if still clean.
- No-ops until WARDEN_MODEL, WARDEN_ANTHROPIC_API_KEY, WARDEN_APP_ID,
  and WARDEN_PRIVATE_KEY secrets are configured.

* ci: close clearance gaps in Warden workflows

- Drop ready_for_review trigger: warden.toml cannot match it, so the
  run would report findings_count=0 without analyzing and wrongly
  grant clearance on draft-to-ready PRs.
- Skip fork PRs in warden.yml (no secrets on forks; clearance already
  fork-blocked downstream) instead of failing red.
- Add timeout-minutes to both jobs and serialize clearance runs per
  branch (queued, not cancelled, so revokes are never killed).

* ci: switch Warden to openai/gpt-5.6-luna

Agent and auxiliary lanes both use gpt-5.6-luna: auxiliary verification
gates which findings survive, so it stays on the same strong model to
avoid false clearances. Secret becomes WARDEN_OPENAI_API_KEY.

Validated locally against the real CLI: injection+XSS diff -> 1 high
(same-root-cause merge), XSS-only diff -> 1 high, clean diff -> 0
findings. 3-7s per run.

* ci: trigger Warden analyze with live secrets

* ci: pin Warden workflow actions to commit SHAs

Addresses Warden's own finding on this PR: the token-minting action
ran from a mutable tag while holding the app private key. All actions
in both workflows are now SHA-pinned.

* ci: grant contents:write so Warden can resolve fixed threads

Resolving review threads via GraphQL requires contents:write on the
GITHUB_TOKEN; without it the report step fails after fix evaluation
and stale findings block merge under thread-resolution rules.

* ci: set least-privilege permissions on electron build workflow

Fixes both open CodeQL actions/missing-workflow-permissions alerts;
neither job writes to the repo or GitHub API.
Seats and model access both cost $10 per user and were rendered as two
near-identical cards, so people bought one believing they had bought the
other — and seat checkout runs in Stripe setup mode ($0 today) while model
access charges the whole team immediately, which the page never said.

Billing now opens on a single statement that names both line items with
their own status and price and sums them into a total, so no charge is
unaccounted for. Each card states who is billed, and every action is paired
with a sentence describing when to use it instead of standing alone as a
bare button. Deployments without Stripe keys no longer quote prices or
offer a checkout the operator cannot complete.

Adds DenUsageMeter, DenActionRow/DenActionList, and DenLineItemRow/DenMarkTile,
and extends DenNotice with neutral and warning tones.

Co-authored-by: Cursor <cursoragent@cursor.com>
… connector room (#3416)

* feat(evals): one-command provisioning for the two-member connector spec

Getting org-connector-two-members green took ~50 minutes of hand
orchestration across four machines, and two of its failures were pure
operator traps. This turns the hands into code:

- test-server-on-daytona.sh --seed: the demo-org seed now runs inside
  start-daytona-server.sh with the exact env the Den API got. Seeded by
  hand with env that diverged (a missing DEN_ORG_MODE), the seed refused
  email signup while the API allowed it.
- @openwork/hosts provisioning primitives, one job each:
  provisionDenSandbox (server script + seeded-org sign-in proof),
  provisionDesktopSandbox (create-or-reuse, checkout, install, disk gate,
  display, xdg-open browser-hop proof, Vite prewarm),
  startMockOnSandbox (public-issuer mock, health + issuer assert),
  deleteSandboxes, renderConnectorSpecEnv/parseConnectorSpecEnv.
  Every remote command travels as ONE bash -lc argument: daytona exec
  joins args with spaces, so anything else silently drops commands.
- scripts/provision-org-connector-two-members.ts: composes the
  primitives for that one spec (1 Den + mock + 2 desktops), writes the
  spec's exact env contract, and --delete removes only what it created.

Six new unit tests pin the traps (single-argument law, disk gate, issuer
mismatch, --yes fallback, env round-trip); 101 pass.

* fix(evals): give the Den sign-in proof a settling window and a voice

First live run: a freshly-booted stack answered public sign-in with bare
403s for its first ~minute, then recovered — and the proof both gave up
at 30s and discarded the response body that would have named the reason.
Widen to 120s and keep the body in the failure message.

* refactor(evals): delete the speculative --yes deletion branch

The installed CLI rejects --yes outright, so the try-flag-then-fall-back
dance made every deletion pay a guaranteed failure first. Always answer
the confirmation prompt on stdin; a promptless CLI ignores it.

* fix(evals): send Origin on the sign-in proof like every real client

The captured body named it: a fresh stack answers origin-less POSTs with
403 MISSING_OR_NULL_ORIGIN for its first minutes. Browsers and the app
always send Origin; only this synthetic probe did not.

* fix(evals): stop the browser-hop gate from pkilling its own shell

The cleanup pattern is a substring of the bash -lc command line carrying
it, so pkill TERMed the gate's shell (exit 143) before the verdict
echoed. Verdict first, bracketed patterns so self-match is impossible.

* fix(evals): browser-hop gate must not run chromium as a child of the exec session

Chromium starting inside a pipe-stdin daytona exec session TERMs the
session (exit 143 ~2.5s in; identical script survives under a TTY). Both
halves now fully detach the way the mock and Vite gates do, and childless
polls read the proof back. Verified through the node exec path on a live
sandbox, which also exercises the reuse path end to end.

* fix(evals): trustworthy checkout pinning and a first-boot gate for desktop rooms

Two failures from live bisecting, both now impossible:

- The checkout gate detached to FETCH_HEAD, which a refused raw-sha fetch
  leaves stale — the sandbox silently ran the wrong tree and the 'resolved'
  log line lied. It now checks out the requested ref (remote-tracking
  first), falls back to a full fetch, and ASSERTS the resolved sha when a
  sha was asked for. The assertion caught two mispins within the hour.

- A sandbox's first Electron boot pays sidecar prepare, the
  openwork-server tsc build, and the engine cold start. Paid inside a
  spec, that bill starved the tool-call phase past its 240s window while
  every UI assertion still passed — reading exactly like a product
  regression (an A/B/A repin exonerated the suspect app commit). The
  provisioner now boots a throwaway profile once, waits for CDP, and
  tears it down: the room is only ready when the app has booted.

* fix(evals): parse the env header by line scan, not backtracking regex

CodeQL js/polynomial-redos: '.*; ref=' backtracks polynomially on a header
padded with repeated '; ref=a'. Scan for the prefix and take the last
marker instead, with render and parse sharing the literals so they cannot
drift. The adversarial input now parses in ~1ms.
…able without a marketplace (skill-sharing P1) (#3410)

* feat(den-api): grant-native capability index — skills usable without a marketplace (skill-sharing P1)

* test(specs): prove grant-native skill access over the real MCP wire (skill-sharing P1)

* test(evals): self-diagnosing member bootstrap for single-org Dens
…ll share offer, duplicate-plugin 409 (skill-sharing P2) (#3422)
…dge (P3 PR1) (#3424)

* feat(den): team access visibility — aggregate edge endpoint + team detail screen (skill-sharing P3 PR1)

* fix(evals): navigate via CDP Page.navigate parameter instead of constructed code (CodeQL js/bad-code-sanitization)
* feat(den-web): make the plugin catalogue readable

The marketplace and plugin screens identified every entry with a colour slab
hashed from its id, so a catalogue of real products looked like a swatch
palette, and the two-column card grid meant counts never lined up.

Replace the rail with the entry's own logo (marketplaces have one and it was
going unused) or a monogram, and render each surface as a single divided list
where counts share one column. Lead the marketplace directory with the
catalogues that actually contain something instead of whatever was created
last.

Invert the cloud-readiness badge: it announced "Cloud ready" — true of every
row, so it said nothing — while suppressing the two states that need a human.
It is now silent when a plugin is ready and speaks only when action is needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(specs): marketplace-catalog-legibility as a photo-roll spec, retiring the legacy flow

The catalogue redesign proof moves from evals/flows (runner flow + voiceover)
to evals/specs/marketplace-catalog-legibility.slow.test.ts: the same six
claims — per-entry identity, one aligned count column, stocked-first order,
greyed zeros, readiness that only speaks when a plugin needs action, and a
plugin detail that opens with the identity the person clicked — now driven
through a real Chrome against den-web and captured as a photo roll with
vision-validated frames.

signInInBrowser also accepts landing on /dashboard as a sign-in outcome:
plain web sign-in redirects there, while the desktop handoff keeps showing
'signed in' / the openwork:// code.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…role choice, blast-radius archive (skill-sharing P3 PR2) (#3425)
…provenance debt retirement (skill-sharing P3 PR3) (#3429)
…mber signs in to each, agent picks by name (#3427)

* feat(den-api): record the connected Google account, offer the account chooser, and stop deep-linking to u/0

Foundation for multiple Google Workspace connectors per organization. No
user-visible multi-account behavior yet; every change here is additive and
backward compatible.

- external_mcp_connection gains kind + native_provider_key so a row can
  represent a native OAuth connector instead of an external MCP server
  (migration 0050, additive, no constraints and no backfill)
- the native OAuth callback now resolves the account's email from the
  id_token, falling back to the provider's userinfo endpoint, and persists
  it as externalAccountId. Resolution is strictly best-effort: a missing
  claim, a failed fetch, or a junk response still completes the connect
  exactly as before
- Google's authorize prompt becomes "consent select_account" so a member
  who is already signed in is actually offered the account chooser
- Gmail draft and thread links use authuser=<email> instead of the
  hardcoded mail/u/0, which resolves to whichever Google account happens to
  be first in the browser. Falls back to the exact previous u/0 output when
  the email is unknown

The redirect_uri, every route path, and all generated capability names are
unchanged.

* test(evals): drop the stub multi-account flow, keep the approved script

`pnpm fraimz --all` runs every file in evals/flows on the nightly lane, so a
scaffolded flow whose frames are still `ctx.assert(false)` turns nightly red
until the feature lands.

New app-driving proof belongs in evals/specs as a photo-roll spec (see
org-connector-two-members.slow.test.ts and the pr-photo-roll skill), which the
nightly spec lane runs separately and which publishes evidence to the PR. The
voiceover script stays as the approved narration for that spec.

* test(evals): red spec for two Google domains, plus the primitive surfaces it needs

Writes the proof before the feature. The spec enacts the approved narration in
evals/voiceovers/google-workspace-multi-account.md and encodes the definition
of done, including the halves that are easy to skip:

- the mailbox is the witness: the supplier draft must appear in the Labs
  mailbox AND the Robotics mailbox must be empty
- the two connectors' drafts must not share a credential token
- the user-facing request must not contain either connector id, so "the agent
  picked the right connector" cannot pass by being told the answer
- the Gmail link must carry authuser= and must not be /mail/u/0/
- a single-domain organization must still see exactly one card and one button

Six primitives it needs do not exist yet. They are declared here as typed
stubs that throw a message naming the primitive and how to build it, so the
spec compiles and fails for the right reason instead of failing to compile:
startMockGoogle, createNativeConnector, readSessionToolCalls, provisionOrg.
openConnectionsSurface/waitForConnectionCard/revealText are promoted out of
org-connector-two-members rather than stubbed, since working copies existed.

Gated behind OPENWORK_EVAL_GOOGLE_MULTI_ACCOUNT_SPEC so an unconfigured run
skips cleanly; it is .slow, so it never enters the PR lane.

* test(evals): implement startMockGoogle — a Google fixture that can tell two accounts apart

The first of the six primitives the multi-account spec needs. Written as a
focused mock beside scripts/mock-oauth-mcp-server.mjs rather than an extension
of it: six existing eval flows depend on that script's single-account
behaviour, and the safest way to not regress them is to leave it byte-unchanged.

The fixture provides what the old one structurally could not:
- an id_token carrying the authorizing account's email, plus /userinfo, so Den
  can record WHICH account connected
- a real chooser page when prompt contains select_account, with chooseAccount()
  resolving only once the callback has actually been served, never on a timer
- per-account mailboxes, every draft attributed to the credential that created
  it, so a spec can assert the draft landed in one inbox and not the other

draftsFor() returns promptly with [] for an empty mailbox instead of blocking
for the full timeout, because the negative half of the isolation assertion is
the half that matters and a primitive that makes it slow is a primitive that
makes it get dropped.

* feat(den-api): native provider connectors — many Google domains per organization

An organization can now register more than one Google Workspace connector.
A connector is an external_mcp_connection row with kind='native_provider':
its OAuth client lives in org_oauth_client and each member's tokens live in
connected_account, both keyed by the connector row id. The existing
(org_membership_id, provider_id) unique index then makes per-connector
isolation automatic rather than something to enforce.

Backward compatibility is by alias, not migration. An organization that
already configured Google keeps provider_id='google-workspace' literally in
both tables and keeps appearing with id "google-workspace". Nothing is
rewritten, because rewriting provider_id would look like an OAuth client
rotation and revoke every member's tokens.

The redirect URI stays keyed by provider, not by connector, so customers do
not have to touch the Google Cloud Console entry they already pasted. The
connector id travels in the signed OAuth state instead.

Native rows are fenced out of every MCP handshake path — transport, catalog,
tool calls, connect/start discovery, the external OAuth callback binding, and
plugin MCP requirement binding — since a Google connector has no MCP server
behind it and would otherwise fail deep inside a handshake.

Capability namespacing is deliberately not here: un-namespaced Google
capability names still resolve to the organization's default connector.

test(evals): implement provisionOrg, and make the local Den reproducible

provisionOrg creates a real isolated organization with a known shape so a
spec can assert that a single-domain company still sees exactly one card.
evals/scripts/dev-den.ts captures the setup that previously took manual
archaeology: db:push rather than db:migrate (the chain cannot build a
database from scratch), an isolated port and database, and the trusted
origins that otherwise fail as a 403 at sign-in looking like a product bug.

* test(evals): implement createNativeConnector, and ask for a feature that exists

The spec was requesting a "gmail" scope feature that the provider registry
does not define, and the route correctly rejected it. The first fix attempt
added "gmail" to NATIVE_OAUTH_PROVIDERS as an alias for the compose scope,
which is product code bending to fit a test; reverted. The spec now asks for
gmailDraft, which is the real feature name for creating drafts.

createNativeConnector posts the connector and its OAuth client through the
route that landed with the server work, and deleteConnection is confirmed to
work on native rows so the spec's onTestFinished cleanup is real.

* test(evals): frame the pixels, and claim what the product actually shows

Two fixes found by running the spec against the real app rather than reasoning
about it.

The DOM assertions passed while the frames failed: the connection card was in
the document but not on screen, so the vision model saw a truncated workspace
name instead. Every screenshot now reveals its subject first, which is what
revealText exists for — waitForText proves the DOM contains it, a frame proves
pixels.

The first frame also claimed Acme Robotics was "set up", which conflated the
admin having registered the connector with the member having connected their
own account. At that point in the story Jordan has not connected, and the app
correctly offers her a sign-in. The claim now says what a person would see.

Frames 1 and 2 pass: two separate Google connections visible side by side.

* test(evals): assert what the desktop actually offers, and drop an incidental layout claim

Two more corrections found by running against the real app.

The list surface offers each connection a "Sign in" next action and opens a
detail panel to authorize; "Connect your account" (mcp.org_connection_connect_action)
only exists in that panel. The third frame was asserting the panel's wording
against the list, so it counted zero buttons. It now asserts the two cards plus
the sign-in affordance, and the later connect flow already goes through the
detail panel correctly.

The second frame claimed the two connections were "side by side". They are
stacked vertically, and the vision model was right to say so — it passed once
and then failed on the same pixels, because the claim asserted incidental
layout rather than the fact being proven. Claims should describe what a human
would verify, or they are flaky by construction.

* feat(desktop,den-api): say which service a connector fronts, and which account is signed in

The member card for a native connector was anonymous: an admin names it
"Acme Robotics", and nothing on screen said Google Workspace. The vision
check caught this as a real product gap — it passed or failed depending on
whether the model credited the G favicon, which is what a human reviewer
would waver on too.

The usable-connections entries now carry nativeProviderKey (both the
toConnectionResponse mapper and buildNativeProviderEntry — native rows are
served by the latter, which is why fixing only the former changed nothing),
plus the caller's connected account email. The desktop prefixes the card
description with the service name and, once connected, appends
"Signed in as <email>." — keeping the existing sentence intact as a prefix
because other specs wait for it verbatim.

Also: frames now assert prompt CONTAINS select_account (the product
correctly sends "consent select_account"), the detail-panel frame replaces
a pixel-duplicate list frame, and openConnectionsSurface steers back in a
loop — the app opens a freshly created session on its own, and a single
navigation followed by a 60s wait parks the spec on /session forever. Its
failure now reports what was actually on screen.

* feat(desktop): badge each connected connector with its signed-in account

"Connected" alone cannot tell two Google sign-ins apart, and the approved
demo narration promises the list shows the real email behind each connector.
The connected badge now reads "Connected — jordan@acmelabs.test" when the
account identity is known.

The spec's both-emails frame closes the Labs panel first and waits for both
addresses on the list before framing it.

* chore(den-db): regenerate connector-kind migration as 0051 after dev landed its own 0050

* fix(den-api): enforce member access grants on native connector OAuth (connect/start, callback, status, disconnect, default resolution)

Warden NPN-7MR: an ungranted member could begin OAuth on a native connector
and back un-namespaced Google capabilities with it. Every member-facing
resolution of a connector row now goes through the grant-aware listing;
the legacy org_oauth_client-only alias keeps its implicitly org-wide
behavior unchanged.
…search placement, card/chip fidelity; spec now pins design (#3430)
…, named connectors (#3432)

The Google tile stops being a singleton: it always opens the dialog, the
dialog takes a name ("Acme Labs"), and submit creates a native connector
row via POST /v1/mcp-connections instead of overwriting the org's single
legacy client. Legacy rows still edit the old client. Disconnect routing
on web and desktop now decides by nativeProviderKey on the payload rather
than id === "google-workspace", so connector rows disconnect through the
oauth-providers path; Your Connections shows the signed-in email for
Google rows like it already did for Microsoft.
…needs-admin-setup, MCP transport facet in filters (#3434)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants